
package com.whosly.mysql.protocol.jdbc;

import java.io.UnsupportedEncodingException;
import java.math.BigDecimal;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.Calendar;

/**
 * For MySql
 */
public class MySQLMessage {
    public static final long NULL_LENGTH = -1;
    private static final byte[] EMPTY_BYTES = new byte[0];

    private final byte[] data;
    private final int length;
    private int position;

    public MySQLMessage(byte[] data) {
        this.data = data;
        this.length = data.length;
        this.position = 0;
    }

    public int length() {
        return length;
    }

    public int position() {
        return position;
    }

    public byte[] bytes() {
        return data;
    }

    public void move(int i) {
        position += i;
    }

    public void position(int i) {
        this.position = i;
    }

    public boolean hasRemaining() {
        return length > position;
    }

    public byte read(int i) {
        return data[i];
    }

    public byte read() {
        return data[position++];
    }

    public int readUB2() {
        final byte[] b = this.data;
        int i = b[position++] & 0xff;
        i |= (b[position++] & 0xff) << 8;
        return i;
    }

    /**
     * 读取short类型，处理符号位
     * @return
     */
    public int readShort() {
        final byte[] b = this.data;
        int i = b[position++] & 0xff;
        byte high = b[position++];
        i |= (high & 0xff) << 8;
        // 根据符号位填充高位
        byte flag = (byte)(high >> 8);
        i |= (flag) << 16;
        i |= (flag) << 24;
        return i;
    }

    public int readUB3() {
        final byte[] b = this.data;
        int i = b[position++] & 0xff;
        i |= (b[position++] & 0xff) << 8;
        i |= (b[position++] & 0xff) << 16;
        return i;
    }

    public long readUB4() {
        final byte[] b = this.data;
        long l = (long) (b[position++] & 0xff);
        l |= (long) (b[position++] & 0xff) << 8;
        l |= (long) (b[position++] & 0xff) << 16;
        l |= (long) (b[position++] & 0xff) << 24;
        return l;
    }

    public int readInt() {
        final byte[] b = this.data;
        int i = b[position++] & 0xff;
        i |= (b[position++] & 0xff) << 8;
        i |= (b[position++] & 0xff) << 16;
        i |= (b[position++] & 0xff) << 24;
        return i;
    }

    public float readFloat() {
        return Float.intBitsToFloat(readInt());
    }

    public long readLong() {
        final byte[] b = this.data;
        long l = (long) (b[position++] & 0xff);
        l |= (long) (b[position++] & 0xff) << 8;
        l |= (long) (b[position++] & 0xff) << 16;
        l |= (long) (b[position++] & 0xff) << 24;
        l |= (long) (b[position++] & 0xff) << 32;
        l |= (long) (b[position++] & 0xff) << 40;
        l |= (long) (b[position++] & 0xff) << 48;
        l |= (long) (b[position++] & 0xff) << 56;
        return l;
    }

    public double readDouble() {
        return Double.longBitsToDouble(readLong());
    }

    public long readLength() {
        int length = data[position++] & 0xff;
        switch (length) {
            case 251:
                return NULL_LENGTH;
            case 252:
                return readUB2();
            case 253:
                return readUB3();
            case 254:
                return readLong();
            default:
                return length;
        }
    }

    public byte[] readBytes() {
        if (position >= length) {
            return EMPTY_BYTES;
        }
        byte[] ab = new byte[length - position];
        System.arraycopy(data, position, ab, 0, ab.length);
        position = length;
        return ab;
    }

    public byte[] readBytes(int length) {
//        byte[] ab = new byte[length];
//        System.arraycopy(data, position, ab, 0, length);
//        position += length;
        return readBytes(0, length);
    }

    public byte[] readBytes(int offset, int length) {
        byte[] ab = new byte[length];
        System.arraycopy(data, position, ab, offset, length);
        position += length;
        return ab;
    }

    public byte[] readBytesWithNull() {
        final byte[] b = this.data;
        if (position >= length) {
            return EMPTY_BYTES;
        }
        int offset = -1;
        for (int i = position; i < length; i++) {
            if (b[i] == 0) {
                offset = i;
                break;
            }
        }
        switch (offset) {
            case -1:
                byte[] ab1 = new byte[length - position];
                System.arraycopy(b, position, ab1, 0, ab1.length);
                position = length;
                return ab1;
            case 0:
                position++;
                return EMPTY_BYTES;
            default:
                byte[] ab2 = new byte[offset - position];
                System.arraycopy(b, position, ab2, 0, ab2.length);
                position = offset + 1;
                return ab2;
        }
    }

    public byte[] readBytesWithLength() {
        int length = (int) readLength();
        if (length <= 0) {
            return null;
        }
        byte[] ab = new byte[length];
        System.arraycopy(data, position, ab, 0, ab.length);
        position += length;
        return ab;
    }

    public String readString() {
        if (position >= length) {
            return null;
        }
        String s = new String(data, position, length - position);
        position = length;
        return s;
    }

    public String readString(String charset) throws UnsupportedEncodingException {
        if (position >= length) {
            return null;
        }
        String s = new String(data, position, length - position, charset);
        position = length;
        return s;
    }

    public String readStringWithNull() {
        final byte[] b = this.data;
        if (position >= length) {
            return null;
        }
        int offset = -1;
        for (int i = position; i < length; i++) {
            if (b[i] == 0) {
                offset = i;
                break;
            }
        }
        if (offset == -1) {
            String s = new String(b, position, length - position);
            position = length;
            return s;
        }
        if (offset > position) {
            String s = new String(b, position, offset - position);
            position = offset + 1;
            return s;
        } else {
            position++;
            return null;
        }
    }

    public String readStringWithNull(String charset) throws UnsupportedEncodingException {
        final byte[] b = this.data;
        if (position >= length) {
            return null;
        }
        int offset = -1;
        for (int i = position; i < length; i++) {
            if (b[i] == 0) {
                offset = i;
                break;
            }
        }
        switch (offset) {
            case -1:
                String s1 = new String(b, position, length - position, charset);
                position = length;
                return s1;
            case 0:
                position++;
                return null;
            default:
                String s2 = new String(b, position, offset - position, charset);
                position = offset + 1;
                return s2;
        }
    }

    public String readStringWithLength() {
        int length = (int) readLength();
        if (length <= 0) {
            return null;
        }
        String s = new String(data, position, length);
        position += length;
        return s;
    }

    public String readStringWithLength(String encoding) {
        int length = (int) readLength();
        if (length <= 0) {
            return null;
        }

        try {
            if (encoding != null) {
                return new String(data, position, length, encoding);
            } else {
                return new String(data, position, length);
            }
        } catch (UnsupportedEncodingException e) {
            // handle exception
            return new String(data, position, length);
        } finally {
            position += length;
        }
    }

    /**
     * ProtocolBinary::MYSQL_TYPE_TIME:
     * type to store a TIME field in the binary protocol.
     *
     * to save space the packet can be compressed:
     *
     * if days, hours, minutes, seconds and micro_seconds are all 0, length is 0 and no other field is sent
     *
     * if micro_seconds is 0, length is 8 and micro_seconds is not sent
     *
     * otherwise length is 12
     *
     * Fields
     * length (1) -- number of bytes following (valid values: 0, 8, 12)
     *
     * is_negative (1) -- (1 if minus, 0 for plus)
     *
     * days (4) -- days
     *
     * hours (1) -- hours
     *
     * minutes (1) -- minutes
     *
     * seconds (1) -- seconds
     *
     * micro_seconds (4) -- micro-seconds
     *
     * Example
     * 0c 01 78 00 00 00 13 1b 1e 01 00 00 00 -- time  -120d 19:27:30.000 001
     * 08 01 78 00 00 00 13 1b 1e             -- time  -120d 19:27:30
     * 01                                     -- time     0d 00:00:00
     * @return
     */
    public java.sql.Time readTime() {
        Calendar cal = Calendar.getInstance();
        cal.clear();
        byte length = read();
        if (length == 0) {
            cal.set(0, 0, 0, 0, 0, 0);
            return new Time(cal.getTimeInMillis());
        }
        // 正负号和天将被忽略
        byte isNegative = read();
        long day = readUB4();
        int hour = read();
        int minute = read();
        int second = read();
        if (length == 8) {
            cal.set(0, 0, 0, hour, minute, second);
            return new Time(cal.getTimeInMillis());
        }
        // length为12的情况
        // prepare协议使用微秒
        long micro = readUB4();
        cal.set(0, 0, 0, hour, minute, second);
        cal.set(Calendar.MILLISECOND, (int) micro / 1000);
        return new Time(cal.getTimeInMillis());
    }

    public java.sql.Date readDate() {
        // Date属于timestamp的分支
        java.sql.Timestamp timestamp = readTimestamp();
        return new java.sql.Date(timestamp.getTime());
    }

    /**
     * ProtocolBinary::MYSQL_TYPE_TIME:
     * type to store a DATE, DATETIME and TIMESTAMP field in the binary protocol.
     *
     * if year, month, day, hour, minutes, seconds and micro_seconds are all 0, length is 0 and no other field is sent
     * if hour, minutes, seconds and micro_seconds are all 0, length is 4 and no other field is sent
     * if micro_seconds is 0, length is 7 and micro_seconds is not sent
     * otherwise length is 11
     *
     * Fields
     * length (1) -- number of bytes following (valid values: 0, 4, 7, 11)
     * year (2) -- year
     * month (1) -- month
     * day (1) -- day
     * hour (1) -- hour
     * minute (1) -- minutes
     * second (1) -- seconds
     * micro_second (4) -- micro-seconds
     *
     * Example
     * 0b da 07 0a 11 13 1b 1e 01 00 00 00 -- datetime 2010-10-17 19:27:30.000 001
     * 04 da 07 0a 11                      -- date = 2010-10-17
     * 0b da 07 0a 11 13 1b 1e 01 00 00 00 -- timestamp
     * @return
     */
    public java.sql.Timestamp readTimestamp() {
        Calendar cal = Calendar.getInstance();
        cal.clear();
        byte length = read();
        if (length == 0) {
            cal.set(0, 0, 0, 0, 0, 0);
            Timestamp time = new Timestamp(cal.getTimeInMillis());
            time.setNanos(0);
            return time;
        }
        int year = readUB2();
        byte month = read();
        byte date = read();
        if (length == 4) {
            cal.set(year, month - 1, date);
            return new java.sql.Timestamp(cal.getTimeInMillis());
        }
        int hour = read();
        int minute = read();
        int second = read();
        if (length == 7){
            cal.set(year, month - 1, date,hour,minute,second);
            return new java.sql.Timestamp(cal.getTimeInMillis());
        }
        // length为11的情况
        // prepare协议使用微秒
        long micro = readUB4();
        cal.set(year, month - 1, date, hour, minute, second);
        Timestamp time = new Timestamp(cal.getTimeInMillis());
        time.setNanos((int) micro * 1000);
        return time;
    }

    public BigDecimal readBigDecimal() {
        String src = readStringWithLength();
        return src == null ? null : new BigDecimal(src);
    }

    public String toString() {
        return new StringBuilder().append(Arrays.toString(data)).toString();
    }

}